package com.encryption.plugin.util;

import com.encryption.plugin.domain.EncryptData;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.*;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;


/**
 * @author liuzhiqiang
 * jar操作工具类
 */
public class JarUtils {

    public static final String[] springLoadJars = {"spring-core-*.jar"};

    /**
     * 打包时需要删除的文件
     */
    public static final String[] DLE_FILES = {".DS_Store", "Thumbs.db"};

    /**
     * 重写的类
     */
    public static final String[] overwriteClass = {
            "\\org\\springframework\\asm\\ClassReader.class",
            "org\\springframework\\core\\io\\ClassPathResource.class"
    };

    /**
     * 加密数据
     */
    private EncryptData encryptData;

    /**
     * 目标jar
     */
    private String targetJar;

    /**
     * jar解压后目录
     */
    private File targetDir;

    /**
     * jar解压后的lib目录
     */
    private File targetLibDir;

    /**
     * jar解压后的Classes目录
     */
    private File targetClassesDir;


    public JarUtils(EncryptData encryptData, String targetJar) {
        this.targetJar = targetJar;
        this.encryptData = encryptData;

        this.targetDir = new File(targetJar.replace(".jar", "__temp__"));
        this.targetLibDir = new File(this.targetDir, "BOOT-INF" + File.separator + "lib");
        this.targetClassesDir = new File(this.targetDir, "BOOT-INF" + File.separator + "classes");

        Log.info("参数:" + this.encryptData.toString());
    }

    /**
     * 释放jar内以及子jar的所有文件
     *
     * @param jarPath   jar文件
     * @param targetDir 释放文件夹
     * @return 所有文件的完整路径，包含目录
     */
    public static List<String> unJar(String jarPath, String targetDir) {
        return unJar(jarPath, targetDir, null);
    }

    /**
     * 释放jar内以及子jar的所有文件
     *
     * @param jarPath   jar文件
     * @param targetDir 释放文件夹
     * @return 所有文件的完整路径，包含目录
     */
    public static List<String> unJar(String jarPath, String targetDir, List<String> includeFiles) {
        List<String> list = new ArrayList<>();
        File target = new File(targetDir);
        if (!target.exists()) {
            target.mkdirs();
        }

        FileInputStream fin = null;
        ZipFile zipFile = null;
        try {
            zipFile = new ZipFile(new File(jarPath));
            ZipEntry entry;
            File targetFile;
            //先把文件夹创建出来
            Enumeration<?> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                entry = (ZipEntry) entries.nextElement();
                if (entry.isDirectory()) {
                    targetFile = new File(target, entry.getName());
                    if (!targetFile.exists()) {
                        targetFile.mkdirs();
                    }
                } else {//有时候entries没有目录,根据文件路径创建目录
                    int lastSeparatorIndex = entry.getName().lastIndexOf("/");
                    if (lastSeparatorIndex > 0) {
                        targetFile = new File(target, entry.getName().substring(0, lastSeparatorIndex));
                        if (!targetFile.exists()) {
                            targetFile.mkdirs();
                        }
                    }
                }
            }

            //再释放文件
            entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                entry = (ZipEntry) entries.nextElement();
                if (entry.isDirectory()) {
                    continue;
                }
                targetFile = new File(target, entry.getName());

                //跳过未包含的文件
                if (includeFiles != null && includeFiles.size() > 0 && !includeFiles.contains(targetFile.getName())) {
                    continue;
                }
                byte[] bytes = IoUtils.toBytes(zipFile.getInputStream(entry));
                IoUtils.writeFile(targetFile, bytes);
                list.add(targetFile.getAbsolutePath());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            IoUtils.close(zipFile, fin);
        }
        return list;
    }

    /**
     * 把目录压缩成jar
     *
     * @param jarDir    需要打包的目录
     * @param targetJar 打包出的jar/war文件路径
     * @return 打包出的jar/war文件路径
     */
    public static String doJar(String jarDir, String targetJar) {
        File jarDirFile = new File(jarDir);
        //枚举jarDir下的所有文件以及目录
        List<File> files = new ArrayList<>();
        IoUtils.listFile(files, jarDirFile);

        ZipOutputStream zos = null;
        OutputStream out = null;

        try {
            File jar = new File(targetJar);
            if (jar.exists()) {
                jar.delete();
            }

            out = new FileOutputStream(jar);
            zos = new ZipOutputStream(out);

            for (File file : files) {
                if (isDel(file)) {
                    continue;
                }
                String fileName = file.getAbsolutePath().substring(jarDirFile.getAbsolutePath().length() + 1);
                fileName = fileName.replace(File.separator, "/");
                //目录，添加一个目录entry
                if (file.isDirectory()) {
                    ZipEntry ze = new ZipEntry(fileName + "/");
                    ze.setTime(System.currentTimeMillis());
                    zos.putNextEntry(ze);
                    zos.closeEntry();
                }
                //jar文件，需要写crc信息
                else if (fileName.endsWith(".jar")) {
                    byte[] bytes = IoUtils.readFileToByte(file);
                    ZipEntry ze = new ZipEntry(fileName);
                    ze.setMethod(ZipEntry.STORED);
                    ze.setSize(bytes.length);
                    ze.setTime(System.currentTimeMillis());
                    ze.setCrc(IoUtils.crc32(bytes));
                    zos.putNextEntry(ze);
                    zos.write(bytes);
                    zos.closeEntry();
                }
                //其他文件直接写入
                else {
                    ZipEntry ze = new ZipEntry(fileName);
                    ze.setTime(System.currentTimeMillis());
                    zos.putNextEntry(ze);
                    byte[] bytes = IoUtils.readFileToByte(file);
                    zos.write(bytes);
                    zos.closeEntry();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            IoUtils.close(zos, out);
        }
        return targetJar;
    }

    /**
     * 是否删除这个文件
     *
     * @param file 文件
     * @return 是否需要删除
     */
    public static boolean isDel(File file) {
        for (String f : DLE_FILES) {
            if (file.getAbsolutePath().endsWith(f)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取class运行的classes目录或所在的jar包目录
     *
     * @return 路径字符串
     */
    public static String getRootPath(String path) {
        if (path == null) {
            path = JarUtils.class.getResource("").getPath();
        }

        try {
            path = java.net.URLDecoder.decode(path, "utf-8");
        } catch (UnsupportedEncodingException e) {
        }

        if (path.startsWith("jar:") || path.startsWith("war:")) {
            path = path.substring(4);
        }
        if (path.startsWith("file:")) {
            path = path.substring(5);
        }

        //没解压的war包
        if (path.contains("*")) {
            return path.substring(0, path.indexOf("*"));
        }
        //war包解压后的WEB-INF
        else if (path.contains("WEB-INF")) {
            return path.substring(0, path.indexOf("WEB-INF"));
        }
        //jar
        else if (path.contains("!")) {
            return path.substring(0, path.indexOf("!"));
        }
        //普通jar/war
        else if (path.endsWith(".jar") || path.endsWith(".war")) {
            return path;
        }
        //no
        else if (path.contains("/classes/")) {
            return path.substring(0, path.indexOf("/classes/") + 9);
        }
        return null;
    }

    /**
     * 加密jar
     */
    public void encryptionJar() {

        // 第一步--解压jar
        Log.info("第一步--解压jar");
        List<String> allFile = unJar(this.targetJar, this.targetDir.getAbsolutePath());
        // 第二部--解压需要额外处理的jar
        Log.info("第二部--解压需要额外处理的jar");
        List<String> libJarFiles = new ArrayList<>();
        for (String path : allFile) {
            if (!path.toLowerCase().endsWith(".jar")) {
                continue;
            }
            String name = path.substring(path.lastIndexOf(File.separator) + 1);
            if (StrUtils.isMatchs(Arrays.asList(springLoadJars), name, false)) {
                String targetPath = path.substring(0, path.length() - 4) + "__temp__";
                List<String> files = JarUtils.unJar(path, targetPath);
                libJarFiles.add(path);
                libJarFiles.addAll(files);
            }
        }
        allFile.addAll(libJarFiles);

        for (String libJarFile : libJarFiles) {
            for (String aClass : overwriteClass) {
                if (libJarFile.contains(aClass)) {
                    ClassPool classPool = ClassPool.getDefault();
                    try {
                        CtClass ctClass = classPool.makeClass(new FileInputStream(libJarFile));

                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    Log.info(libJarFile);
                }
            }
        }

        String result = packageJar(libJarFiles);
        Log.info("加密后的jar:" + result);
    }

    /**
     * 压缩成jar
     *
     * @return 打包后的jar绝对路径
     */
    private String packageJar(List<String> libJarFiles) {
        //[1]先打包lib下的jar
        for (String targetJar : libJarFiles) {
            if (!targetJar.endsWith(".jar")) {
                continue;
            }

            String srcJarDir = targetJar.substring(0, targetJar.length() - 4) + "__temp__";
            if (!new File(srcJarDir).exists()) {
                continue;
            }
            JarUtils.doJar(srcJarDir, targetJar);
            IoUtils.delete(new File(srcJarDir));
            Log.info("打包: " + targetJar);
        }

        //删除META-INF下的maven
        IoUtils.delete(new File(this.targetDir, "META-INF/maven"));

        //[2]再打包jar
        String targetJar = this.targetJar.replace(".jar", "-encrypted." + "jar");
        String result = JarUtils.doJar(this.targetDir.getAbsolutePath(), targetJar);
        IoUtils.delete(this.targetDir);
        Log.info("打包: " + targetJar);
        return result;
    }

    /**
     * 读取流
     *
     * @param inStream
     * @return 字节数组
     * @throws Exception
     */
    public byte[] readStream(InputStream inStream) throws Exception {
        ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inStream.read(buffer)) != -1) {
            outSteam.write(buffer, 0, len);
        }
        outSteam.close();
        inStream.close();
        return outSteam.toByteArray();
    }

    /**
     * 加密Jar包里的文件
     *
     * @throws Exception
     */
    public void writeJarFile() throws Exception {

        //1、首先将原Jar包里的所有内容读取到内存里，用TreeMap保存
        JarFile jarFile = new JarFile(this.targetJar);
        //可以保持排列的顺序,所以用TreeMap 而不用HashMap
        TreeMap tm = new TreeMap();
        Enumeration es = jarFile.entries();
        while (es.hasMoreElements()) {
            Map<String, Object> map = new HashMap<>();
            JarEntry entry = (JarEntry) es.nextElement();
            map.put("entry", entry);

            String name = entry.getName();

            byte[] bytes = readStream(jarFile.getInputStream(entry));

            Boolean on = false;
            if (!StringUtils.endsWith(name, "/")) {
                // 文件
                String fileSuffix = name.split("\\.")[name.split("\\.").length - 1];
                for (String packagePath : this.encryptData.getPackages()) {
                    packagePath = packagePath.replaceAll("\\.", "\\/");
                    packagePath = "BOOT-INF/classes/" + packagePath;
                    if (StringUtils.contains(name, packagePath) && ArrayUtils.contains(this.encryptData.getSuffix(), fileSuffix)) {
                        on = true;
                    }
                }
                String fileName = name.split("/")[name.split("/").length - 1];
                if (ArrayUtils.contains(this.encryptData.getFiles(), fileName)) {
                    on = true;
                }
            }

            if (on) {
                // 密文
                byte[] cipherText = DESUtil.encryptByte(this.encryptData.getPassword(), bytes);
                map.put("bytes", cipherText);
                tm.put(name, map);
                System.out.println("加密文件[" + name + "]");
            } else {
                map.put("bytes", bytes);
                tm.put(name, map);
            }
        }

        JarOutputStream jos = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(this.targetJar)));
        Iterator it = tm.entrySet().iterator();

        //2、将TreeMap重新写到原jar里，如果TreeMap里已经有entryName文件那么覆盖，否则在最后添加
        while (it.hasNext()) {
            Map.Entry item = (Map.Entry) it.next();
            String name = (String) item.getKey();
            Map<String, Object> map = (Map<String, Object>) item.getValue();
            JarEntry entry = (JarEntry) map.get("entry");
            byte[] temp = (byte[]) map.get("bytes");
            JarEntry jarEntry = new JarEntry(name);
            jarEntry.setMethod(entry.getMethod());
            jarEntry.setSize(entry.getSize());
            jarEntry.setCrc(entry.getCrc());
            jos.putNextEntry(jarEntry);
            jos.write(temp, 0, temp.length);
        }
        //3. 将证书文件添加至jar包
        JarEntry jarEntry = new JarEntry("BOOT-INF/classes/encryption.certificate");
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(this.encryptData);
        byte[] temp = byteArrayOutputStream.toByteArray();
        jos.putNextEntry(jarEntry);
        jos.write(temp, 0, temp.length);

        jos.finish();
        jos.close();


    }

}
